#!/usr/bin/env python
# -*- mode: python; coding: utf-8; -*-
##--------------------------------------------------------------------------##
##
## Copyright (C) 1998-2003 Markus Oberhumer <markus@oberhumer.com>
## Copyright (C) 2003 Mt. Hood Playing Card Co.
## Copyright (C) 2005-2010 Skomoroh <skomoroh@users.sourceforge.net>
## Copyright (C) 2010-2013 Jeremy Austin-Bardo <tjaustinbardo@gmail.com>
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program.  If not, see <http://www.gnu.org/licenses/>.
##
##--------------------------------------------------------------------------##
"""
Game subclass for persistance of games. Handles load/dump of prior game details.
"""
__docformat__ = "restructuredtext en"
#_ = lambda n: n

import abc
from contextlib import closing
from cPickle import Pickler, Unpickler, UnpicklingError, PicklingError
from cStringIO import StringIO
import logging

LOGGER = logging.getLogger('NPS.GameLib.StoreMixin')

from .data import GAME_DB
from ..settings import REDO, UNDO, PACKAGE, VERSION, VERSION_TUPLE
from ..viewlib.dialog import Dialog
from . import group as GI
from . import state
from ..pilelib import move as mv

class GameStore(object):
    """
    Handle game data for persistance.

    Variables:
      GameStore.__VERSION__ - Overide in game subclass version to verify
      compatible with with game loaded.

      GameStore._can_save_ - Overide in subclass to indicate persistance.

      GameStore.__status - Additional persistent game details to restore.

      GameStore.filename - Filename to save game data to disk storage.
    """
    __metaclass__ = abc.ABCMeta
    __VERSION__ = 7.70
    __status = None
    _can_save_ = True
    filename = ""

    finished = preview = False
    gstats, stats, moves, s = {}, {}, {}, {}
    app = move_hints = random = None
    allstacks = []

    #@abc.abstractmethod
    def _move_do(self, atomic_move, reset=False):
        """
        Perform low-level atomic move.

        Stub to perform atomic move. Calls next method in order.

        *Note:* Method defined within mix-in GameMove module in library.

        .. do_update_status(flags:int) -> None
        """
        ancestor = super(GameStore, self)
        if hasattr(ancestor, '_move_do'):
            return ancestor._move_do(atomic_move, reset=reset)

    @property
    def status(self):
        """
        Get game details that need persistance after a game restore. Useful for
        games that restrict play to certain events before another can happen.

        .. status -> *

        *Parameters*:
            status: {any}
              Return value[s] as sequence or mapping that need to persist.

        *Requires*:
          Variables:
            GameStore.__status - Get status value[s].
        """
        return self.__status

    @status.setter
    def status(self, status):
        """
        Set game details that need persistance after a game restore. Useful for
        games that restrict play to certain events before another can happen.

        *note*:
          Method is for general use with game subclasses as it does add
          a move history event which is needed for undo/redo support.

        .. status = *

        *Parameters*:
            status: {any}
              Set given value[s] as sequence or mapping that need to persist.

        *Requires*:
          Variables:
            GameStore.__status - set value of given status.
        """
        self._move_do(mv.ASaveStatusMove(self.status, UNDO))
        self.__status = status
        self._move_do(mv.ASaveStatusMove(self.status, REDO))

    @state.game_busy
    def prepare(self, progress=None):
        """
        Prepare filename for current game.

        Reset filename. Calls next method in order.

        .. prepare([progress:Progress=None]):

        *Parameters*:
          progress: {Progress} None
             When given value is not none, update progress.

        *Requires*:
            Variables:
              Game.filename - reset value to empty text.
        """
        ancestor = super(GameStore, self)
        if hasattr(ancestor, 'prepare'):
            ancestor.prepare(progress)
        self.filename = ''


    def reset(self, restart=False):
        """
        Reset or restart current game without reset of some game details. Mix-in
        reset filename and status. Calls next method in resolution order.

        .. reset(restart:bool=False) -> None

        *Parameters*:
          restart: {bool} True
             Control what game details are reset

        *Requires*:
            Variables:
              Game.filename - reset value to empty text.

              GameStore.__status - reset value to null.
        """
        ancestor = super(GameStore, self)
        if hasattr(ancestor, 'reset'):
            ancestor.reset(restart)
        self.filename = ''
        self.__status = None

    @state.game_busy
    def restore(self, data, reset=True, autoplay=True):
        """
        Restore prior game details.

        Augment to set status map with given data. Call next method in order.

        .. restore(data:GameData[, reset:bool=True, autoplay:bool:True]) -> None

        *Parameters*:
          data: {GameData}
            Data to restore game pile card and capability details.
          reset: {boolean}
            Reset current game before restore.
          autoplay: {boolean}
            Call auto play method before return.

        *Requires*:
          Methods:
            Game.set_status.

          Variables:
            Game.stats - set value given in game.stats attribute.

            gstats - set value given in game.gstats attribute.
        """
        ancestor = super(GameStore, self)
        if hasattr(ancestor, 'restore'):
            ancestor.restore(data, reset)

        self.set_status(data.status)

    def set_status(self, status):
        """
        Set game details that need persistance after a game restore. Useful for
        games that restrict play to certain events before another can happen.

        *note*:
          Method is only for its atomic move and loading game as it does not add
          a move history event which is needed for undo/redo support.

        .. set_status(status:*) -> None

        *Parameters*:
            status: {any}
              Set given value[s] as sequence or mapping that need to persist.

        *Requires*:
          Variables:
            GameStore.__status - set value of given status.
        """
        self.__status = status

    def _save_stack_caps(self, piles, **kwargs):
        """
        Set piles' capabilities passed as keywords for persistance after a
        game restore.

        .. \_save_stack_caps(piles:seq(Pile ...)[*pile capabilty keywords*])
           -> None

        *Parameters*:
            pile: {Pile | sequence}
               Given pile or group of piles to have given capability keywords
               applied. Useful for games that assign capabilities after start.
            *kwargs*:
              Capability keywords for a Pile. See ..pilelib.caps.CapStack.
        """
        if not isinstance(piles, (list, tuple)):
            piles = [piles]
        for pile in piles:
            pile.cap.update(**kwargs)

    def _load_data(self, filename=None, bookmark=None):
        """
        Load game data from filname or bookmark. Returns game data to
        restore game.

        .. \_load_data(filename:str|bookmark:int) -> Game Data

        *Parameters*:
          [filename | bookmark]: {text | number}
            Value to indicate where to load. Only one of the keywords may be
            given in call.

        *Requires*:
          Methods:
            [c|]pickle.Unpickler.load; [c|]pickle.UnpicklingError;
            ..viewlib.dialog.Dialog.error; stringio.StringIO;
            contextlib.closing;
            Game.app.construct_game; GamePile.restore_pile.
          Variables:
            Game.app.nextgame['bookmark'] - When given bookmark, load
            result for keyword of value.

            settings.VERSION_TUPLE; settings.PACKAGE;
            GameStore.__VERSION__ - Allows data verification.

            additional values to game always restored - GamePile.random,
             Game._restore_round, GameMove._move

            additional values to game restored based update_stats -
            GameStore.status.

        *Raises*:
          ExceptionError:
            When given both filename and bookmark (Only 1 maybe given.).
        """
        if filename is not None and bookmark is None:
            context = open(filename,  "rb")
        elif bookmark is not None and filename is None:
            assert bookmark in range(10)
            context = StringIO(self.nextgame['bookmark'][bookmark])
        else:
            raise Exception(_('only 1 keyword may be passed'))

        error_text = _("Invalid value found in {} save file").format(PACKAGE)
        update_text = _("Prior {} v{} cannot be loaded. {}")

        # read data
        self.log()
        game = None
        with closing(context) as file_:
            try:
                pkl = Unpickler(file_)

                def pload(type_=None):
                    """
                    Load pickle object optionally checking its given type.
                    """
                    value = pkl.load()
                    if isinstance(type_, type):
                        assert isinstance(value, type_), error_text
                    return value

                # program
                package = pload(str)
                version = pload(str)
                version_tuple = pload(tuple)
                if version_tuple < VERSION_TUPLE or package != PACKAGE:
                    raise UnpicklingError(update_text.format(package, version,
                                                        'Program has changed.'))
                # game
                version = pload(float)
                game_id = pload(int)
                game = self.app.construct_game(game_id)
                if game and not self.__VERSION__ == version:
                    raise UnpicklingError(update_text.format(
                                          game['name'], version,
                                          'Game has changed.'))
                # attribute
                game.random = pload()
                for index in range(pload(int)):
                    self._pack_restore(*pload(tuple))
                game._restore_round = pload(int)
                game._move.update(pload(dict))
                game.status = pload()
                #session
                update_stats = pload(int)
                if not 0 <= update_stats <= 2:
                    raise UnpicklingError(error_text)
                if 0 <= update_stats <= 1:
                    # update values
                    self.nextgame.update(pload(dict))
                    game._timer = pload()
                assert pload(str) == "EOF", error_text

            except (AssertionError, UnpicklingError,) as err:
                Dialog.error(text=_("Error loading game."), err=err)
            except:
                Dialog.error(text=_("Internal error occured."))

        if filename is not None:
            game.filename = filename

        return game

    def _dump_data(self, filename=None, bookmark=None, update_stats=0):
        """
        Dump game data given filename or bookmark and restore later.

        .. \_dump_data(filename:str|bookmark:int[, update_stats:int=0]) -> None

        *Parameters* :
          [filename | bookmark] : {text | number}
            Value to indicate where to dump. Only one of the keywords may be
            given in call.
          update_stats: {number}
            Value given as a flag to indicate how to handle stats and bookmarks.

            *0* - Do dump stats and bookmarks. Do increment saved keyword of
            gstats before.

            *1* - Do dump stats and bookmarks. Do not increment saved keyword
            of gstats before.

            *2* - Do not dump stats and bookmarks. Do not increment saved
            keyword of gstats before.

        *Requires*:
          Methods:
            [c|]pickle.Pickler.dump; [c|]pickle.PicklingError;
            ..viewlib.dialog.Dialog.error; stringio.StringIO;
            contextlib.closing;
          Variables:
            Game.app.nextgame['bookmark'] - When given bookmark, dump
            result for keyword of value.

            settings.VERSION_TUPLE; settings.PACKAGE;
            settings.VERSION - Allow data verification.

            Additional values from Game always persist -
            GameStore.__VERSION__, Game['id'],
            GamePile.random, Game.allstacks, Pile.id, Pile.cap,
            Pile.cards, Card.id, Card.face_up
            Game.s['talon'].round, GameMove._move.

            Additional values from Game persist based on update_stats
            - GameStore.status.

        *Raises*:
          AssertionError:
            When given update_stats is out of range (0..2).
          ExceptionError:
            When given both filename and bookmark (Only 1 maybe given.).
        """
        assert 0 <= update_stats <= 2
        # set mode : file | cache
        if filename is not None and bookmark is None:
            context = open(filename,  "wb")
        elif bookmark is not None and filename is None:
            assert bookmark in range(10)
            context = StringIO()
        else:
            raise Exception(_('only 1 keyword may be passed'))

        # write data
        if not self.is_finish:
            self.log()
        with closing(context) as file_:
            try:
                pkl = Pickler(file_, protocol=-1)
                # format
                pkl.dump(PACKAGE)
                pkl.dump(VERSION)
                pkl.dump(VERSION_TUPLE)
                # version
                pkl.dump(self.__VERSION__)
                pkl.dump(self['id'])
                # attribute
                pkl.dump(self.random)
                pkl.dump(len(self.allstacks))
                for stack in self.allstacks:
                    pkl.dump((stack.id, dict(stack.cap),
                              [(c._id, c.face_up,) for c in stack.cards],))
                pkl.dump(self.s['talon'].round)
                pkl.dump(self._move)
                pkl.dump(self.status)
                # session
                pkl.dump(update_stats)
                if 0 <= update_stats <= 1:
                    nextgame = self.nextgame
                    if update_stats == 0:
                        nextgame['counter']['save'] += 1
                    nextgame = self.nextgame
                    pkl.dump({'bookmark': nextgame['bookmark'],
                              'counter':  nextgame['counter']})
                    pkl.dump(self._timer)

                pkl.dump("EOF")
            except (AssertionError, PicklingError,) as err:
                Dialog.error(text=_("Error saving game."), err=err)
            except:
                Dialog.error(text=_("Internal error occured."))
            else:
                # save cache
                if bookmark is not None:
                    self.nextgame['bookmark'][bookmark] = file_.getvalue()